feat: Update scale event with custom recognizer#3782
feat: Update scale event with custom recognizer#3782stilnat wants to merge 68 commits intoflame-engine:mainfrom
Conversation
… scale-gesture
spydon
left a comment
There was a problem hiding this comment.
Looks good, just a few comments
packages/flame/lib/src/events/flame_game_mixins/multi_drag_dispatcher.dart
Show resolved
Hide resolved
Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
- Add removal guard in ScaleDispatcher.handleScaleStart to reject new gestures after markForRemoval() - Add missing unregisterKey call in ScaleDispatcher.onRemove - Fix comment typos referencing wrong class name in scale_drag_dispatcher - Remove duplicate focal point computation in recognizer _update() - Convert test helper methods to proper extension methods and fix call-sites
Cover markForRemoval guard, deferred removal after active gestures, unregisterKey re-creation, and ScaleDispatcher/MultiDragDispatcher upgrade to MultiDragScaleDispatcher.
Interactive example that lets users spawn drag-only, scale-only, or combined components at runtime using buttons. Useful for verifying that mixing different interaction types works seamlessly.
Add Scale Events to the inputs TOC. Document the isDragged/isScaling properties, combining both mixins on a single component, and dynamic addition of components with different callback types at runtime.
There was a problem hiding this comment.
Pull request overview
This PR refactors how Flame handles simultaneous drag + scale gestures by introducing a dedicated combined gesture recognizer/dispatcher, replacing the previous “recompute scale from drag” approach and improving interoperability between DragCallbacks and ScaleCallbacks.
Changes:
- Added
MultiDragScaleGestureRecognizer+MultiDragScaleDispatcherto recognize/dispatch drag and scale gestures together. - Updated
DragCallbacks/ScaleCallbacksmounting logic to dynamically upgrade dispatchers (and added dispatcher lifecycle tests). - Expanded docs and examples to cover combined and dynamically-added drag/scale components.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/flame/lib/src/events/multi_drag_scale_recognizer.dart | New combined recognizer implementation for multi-drag + scale. |
| packages/flame/lib/src/events/flame_game_mixins/scale_drag_dispatcher.dart | New dispatcher wiring combined recognizer into Flame’s component event system. |
| packages/flame/lib/src/events/flame_game_mixins/multi_drag_dispatcher.dart | Dispatcher lifecycle changes (mark-for-removal), and removed stream APIs. |
| packages/flame/lib/src/events/flame_game_mixins/scale_dispatcher.dart | Removed old “scale-from-drag” recomputation and added mark-for-removal lifecycle. |
| packages/flame/lib/src/events/component_mixins/drag_callbacks.dart | Dispatcher selection/upgrade logic updates for drag/scale coexistence. |
| packages/flame/lib/src/events/component_mixins/scale_callbacks.dart | Dispatcher selection/upgrade logic updates for drag/scale coexistence. |
| packages/flame/lib/events.dart | Exports the new MultiDragScaleDispatcher. |
| packages/flame/test/events/component_mixins/input_test_helper.dart | New shared test helpers for drag/scale gesture simulation and counters. |
| packages/flame/test/events/component_mixins/drag_callbacks_test.dart | Updated to use shared helpers + added lifecycle tests. |
| packages/flame/test/events/component_mixins/scale_callbacks_test.dart | Updated to use shared helpers + added lifecycle tests. |
| packages/flame/test/events/component_mixins/scale_drag_callbacks_test.dart | New test suite validating combined drag+scale behavior and upgrades. |
| doc/flame/inputs/drag_events.md | Documentation for isDragged and combining with scale callbacks. |
| doc/flame/inputs/scale_events.md | Documentation for isScaling and combining with drag callbacks. |
| doc/flame/inputs/inputs.md | Adds Scale Events to the input docs index. |
| examples/lib/stories/input/scale_drag_example.dart | Updated example behavior for combined gestures. |
| examples/lib/stories/input/dynamic_scale_drag_example.dart | New example demonstrating runtime addition/removal of components. |
| examples/lib/stories/input/input.dart | Registers the new example in the story list. |
Comments suppressed due to low confidence (2)
packages/flame/lib/src/events/flame_game_mixins/multi_drag_dispatcher.dart:172
markForRemoval()currently only prevents dispatching inhandleDragStart, but theImmediateMultiDragGestureRecognizerwill still compete in Flutter’s gesture arena and may win new pointers while_shouldBeRemovedis true (causing new drags to be dropped and also blocking the newMultiDragScaleGestureRecognizer). Consider gating the recognizer’sonStartcallback itself (returnnullwhen_shouldBeRemoved), or otherwise ensure the old recognizer no longer claims new pointers once marked for removal.
game.gestureDetectors.add<ImmediateMultiDragGestureRecognizer>(
ImmediateMultiDragGestureRecognizer.new,
(ImmediateMultiDragGestureRecognizer instance) {
instance.onStart = (Offset point) => FlameDragAdapter(this, point);
},
);
examples/lib/stories/input/scale_drag_example.dart:75
- In
update(dt), camera rotation/zoom increments were changed to fixed per-frame deltas (+= 0.001) instead of being scaled bydt. This makes the example frame-rate dependent; consider restoringdt-based updates (or document that the values are intentionally per-frame).
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/flame/lib/src/events/flame_game_mixins/multi_drag_dispatcher.dart
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…izer The property was declared but never used in the recognizer logic. ImmediateMultiDragGestureRecognizer, which this recognizer is modeled after, does not have this property either.
All needed types are already provided by flutter/gestures.dart.
The rotation and zoom increments were accidentally changed to fixed per-frame deltas, making them frame-rate dependent. Restore the original 0.1 * dt scaling.
Prevent creating orphaned FlameDragAdapter instances when the dispatcher is marked for removal during a dispatcher upgrade. Returns null from the recognizer's onStart callback so the pointer is cleanly rejected rather than accepted and silently dropped.
Document on both MultiDragDispatcher and ScaleDispatcher that new gestures are silently dropped during the overlap window while active gestures from the old dispatcher are still completing.
During 2+ pointer scale gestures, drag updates now follow the focal point (center between fingers) instead of an individual pointer. Also fixes inverted drag in the example when a component is rotated.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
packages/flame/test/events/component_mixins/input_test_helper.dart
Outdated
Show resolved
Hide resolved
_delta was computed from _localFocalPoint (after event.transform) but used as ScaleUpdateDetails.focalPointDelta and DragUpdateDetails.delta, both of which pair with global positions. Compute from global _currentFocalPoint instead to keep coordinate spaces consistent.
Replace StackOverflow-sourced ZoomTesting.timedZoomFrom with an original implementation to resolve license incompatibility with the MIT-licensed repo.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| /// **MultiDragDispatcher** facilitates dispatching of drag events to the | ||
| /// [DragCallbacks] components in the component tree. It will be attached to | ||
| /// the [FlameGame] instance automatically whenever any [DragCallbacks] | ||
| /// components are mounted into the component tree. | ||
| class MultiDragDispatcher extends Component implements MultiDragListener { | ||
| /// The record of all components currently being touched. | ||
| final Set<TaggedComponent<DragCallbacks>> _records = {}; | ||
|
|
||
| final _dragUpdateController = StreamController<DragUpdateEvent>.broadcast( | ||
| sync: true, | ||
| ); | ||
|
|
||
| Stream<DragUpdateEvent> get onUpdate => _dragUpdateController.stream; | ||
|
|
||
| final _dragStartController = StreamController<DragStartEvent>.broadcast( | ||
| sync: true, | ||
| ); | ||
|
|
||
| Stream<DragStartEvent> get onStart => _dragStartController.stream; | ||
|
|
||
| final _dragEndController = StreamController<DragEndEvent>.broadcast( | ||
| sync: true, | ||
| ); | ||
|
|
||
| Stream<DragEndEvent> get onEnd => _dragEndController.stream; | ||
|
|
||
| final _dragCancelController = StreamController<DragCancelEvent>.broadcast( | ||
| sync: true, | ||
| ); | ||
|
|
||
| Stream<DragCancelEvent> get onCancel => _dragCancelController.stream; | ||
|
|
||
| FlameGame get game => parent! as FlameGame; | ||
|
|
||
| bool _shouldBeRemoved = false; | ||
|
|
||
| /// Called when the user initiates a drag gesture, for example by touching the |
There was a problem hiding this comment.
This change removes the previously exposed drag event streams (onStart/onUpdate/onEnd/onCancel) from MultiDragDispatcher. Since MultiDragDispatcher is exported from package:flame/events.dart, this is a public API change and may be breaking for downstream users. If the streams are still intended as part of the public surface, consider keeping them (or deprecating first); otherwise consider making the class/internal members non-public or explicitly documenting the breaking change (and updating the PR title/metadata accordingly).
| // Check if we should accept all gestures based on scale threshold | ||
| if (_pointers.length >= 2 && !_scaleGestureActive) { | ||
| _checkScaleGestureThreshold(); | ||
| } | ||
|
|
||
| // Start scale gesture if we now have 2+ pointers | ||
| if (!_scaleGestureActive && _pointers.length >= 2) { | ||
| _scaleGestureActive = true; | ||
| _initialFocalPoint = _currentFocalPoint; | ||
| _initialSpan = _currentSpan; | ||
| _initialHorizontalSpan = _currentHorizontalSpan; | ||
| _initialVerticalSpan = _currentVerticalSpan; | ||
| _initialLine = _currentLine; | ||
| _initialScaleEventTimestamp = event.timeStamp; |
There was a problem hiding this comment.
_checkScaleGestureThreshold() is called before _initialSpan/_initial*Span are initialized for the 2+ pointer case. Since _initialSpan is set when the first pointer is added (typically 0), spanDelta = (_currentSpan - _initialSpan) becomes large as soon as the 2nd pointer moves, causing the arena to be resolved/accepted immediately and effectively bypassing scaleThreshold/slop gating. Consider initializing the “initial” span/focal data when the 2nd pointer is added (or when pointerCount first becomes 2), or move the threshold check to after the block that sets _initialSpan for the scale gesture.
Description
Currently flame handle scale input events in a very hacky way, by getting the drag gesture recognizer data and recomputing
the data for the scale gesture. This has multiple issues :
This PR aims to fix all those issues by introducing a new gesture recognizer, which is basically just a mix of ScaleGestureRecognizer and immediateMultiDragGestureRecognizer, allowing pointers to be used for both gestures
without competing. I used the existing flutter code to write it.
I modified a bit ScaleCallbacks and DragCallbacks, so they use their original dispatcher if there is only one type
of them (it's a bit more efficient), and so they upgrade to using MultiDragScaleDispatcher if both mixins are mounted.
Transition between the two is smooth as the old dispatcher wait for all gestures it started to finish before removing itself.
Checklist
docsand added dartdoc comments with///.examplesordocs.I wonder if gesture_input.md should be updated
Breaking Change?
Related Issues
I believe it Closes #2635